昨天我們設計了分類資料的模型,並初始化了一些預設資料。今天,我們將專注於如何使用這些資料來建立分類列表頁面,讓使用者可以查看並管理他們的分類。
今天的目標是實作一個頁面,使用者可以在這個頁面上瀏覽所有分類,進行刪除操作,並提供一個入口,讓使用者在下一步可以新增分類。這裡 UI 的樣式參考了簡單記帳。
我們先來建立管理分類的 ViewModel – CategoryListViewModel 吧!
這個 ViewModel 將負責處理分類管理頁面的顯示與刪除邏輯,包括從資料庫中載入分類資料、顯示分類列表,以及刪除分類的功能。我們會依賴 DataManager 來與 Core Data 進行互動。
我們需要在 ViewModel 中實作以下功能:
fetchCategories()
,用來從資料庫中載入所有已存在的子分類。deleteCategory( category: ItemCategory)
,負責刪除指定的分類,並在刪除成功後重新抓取資料。如果刪除失敗,則會顯示錯誤訊息。import SwiftUI
class CategoryListViewModel: ObservableObject {
@Published var categories: [ItemCategory] = []
@Published var failHandle: (isFail: Bool, title: String) = (isFail: false, title: "")
private let dataManager: DataManager
init(dataManager: DataManager = DataManager()) {
self.dataManager = dataManager
fetchCategories()
}
func fetchCategories() {
categories = dataManager.fetchItemCategories()
}
func deleteCategory(_ category: ItemCategory) {
let result = dataManager.deleteItemCategory(category)
if !result {
failHandle = (isFail: true, title: "發生錯誤")
} else {
fetchCategories()
}
}
}
接下來,我們將實作畫面,並將 ViewModel 連結到頁面中。
在 CategoryListView 中,每個分類資料會顯示為一個方塊,其中包含 icon 與分類名稱。我們首先來實作 CategoryView,用於顯示單個分類的內容。
我們要讓 CategoryView 接收分類的 icon 和名稱作為輸入,因此我們在 struct 中定義了兩個屬性:iconName 和 name。
struct CategoryView: View {
let iconName: String
let name: String
在 body 中,我們使用了 VStack
來垂直排列 icon 和名稱。這裡我們選擇使用 SF Symbols,並使用 Text
顯示分類名稱。
var body: some View {
VStack {
Image(systemName: iconName) // 使用 SF Symbols icon
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.foregroundColor(Color(.black)) // 設定 icon 顏色
Text(name)
.lineLimit(1) // 限制為一行,避免名稱過長
.truncationMode(.tail) // 當名稱超過一行時,顯示...
.font(.caption) // 設定文字大小
}
resizable()
使 icon 可以調整大小,scaledToFit()
讓圖片保持比例不變,並限制大小為 40x40。lineLimit(1)
保持顯示一行,若名稱過長會用 truncationMode(.tail)
顯示省略符號。接著,我們對整個 VStack
設定框架大小和背景樣式,讓它看起來像一個完整的卡片。
.frame(width: UIScreen.main.bounds.width / 4 - 20, height: UIScreen.main.bounds.width / 4 - 20)
.background(Color.white) // 設定背景為白色
.cornerRadius(10) // 設置圓角
.shadow(radius: 5) // 設定陰影
import SwiftUI
struct CategoryView: View {
let iconName: String
let name: String
var body: some View {
VStack {
Image(systemName: iconName)
.resizable()
.scaledToFit()
.frame(width: 40, height: 40)
.foregroundColor(Color(.black))
Text(name)
.lineLimit(1)
.truncationMode(.tail)
.font(.caption)
}
.frame(width: UIScreen.main.bounds.width / 4 - 20, height: UIScreen.main.bounds.width / 4 - 20)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
}
}
#Preview {
CategoryView(iconName: "fork.knife", name: "廚房用品")
}
CategoryListView 負責顯示目前已建立的分類,並允許使用者進行刪除操作。
在 CategoryListView 中,我們透過 State 變數來管理編輯狀態 (isEditing),並且使用 -@ObservedObject
監聽 CategoryListViewModel 以獲取分類資料。
import SwiftUI
import AlertToast
struct CategoryListView: View {
@State private var isEditing = false
@ObservedObject private var viewModel: CategoryListViewModel
init(viewModel: CategoryListViewModel = CategoryListViewModel()) {
self.viewModel = viewModel
}
}
我們使用 LazyVGrid
來呈現分類,並在分類上方提供刪除按鈕(僅在編輯模式時顯示)。這個設計讓使用者可以方便地瀏覽分類並進行刪除操作。
let columns = [
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
]
var body: some View {
ZStack {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(viewModel.categories, id: \.id) { category in
ZStack(alignment: .topTrailing) {
CategoryView(iconName: category.iconName, name: category.name)
if isEditing {
Button(action: {
viewModel.deleteCategory(category)
}) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.offset(x: 10, y: -10)
}
}
}
}
}
.padding()
.onAppear {
viewModel.fetchCategories()
}
}
}
}
透過 Toolbar
,我們讓使用者可以自由切換編輯模式,並在編輯模式下提供新增分類的入口。
.toolbar {
Button(action: {
isEditing.toggle()
}) {
Image(systemName: isEditing ? "checkmark" : "square.and.pencil")
.font(.title2)
.foregroundColor(.blue)
}
}
當進入編輯模式時,頁面底部會顯示「新增分類」的按鈕,點擊後導覽至 AddCategoryView。
VStack {
Spacer()
if isEditing {
NavigationLink(destination: AddCategoryView()) {
Text("新增分類")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.padding(.horizontal)
}
.padding(.bottom, 20)
}
}
完整程式碼:
import SwiftUI
import AlertToast
struct CategoryListView: View {
@State private var isEditing = false
@ObservedObject private var viewModel: CategoryListViewModel
init(viewModel: CategoryListViewModel = CategoryListViewModel()) {
self.viewModel = viewModel
}
let columns = [
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16),
GridItem(.flexible(), spacing: 16)
]
var body: some View {
ZStack {
ScrollView {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(viewModel.categories, id: \.id) { category in
ZStack(alignment: .topTrailing) {
CategoryView(iconName: category.iconName, name: category.name)
if isEditing {
Button(action: {
viewModel.deleteCategory(category)
}) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.offset(x: 10, y: -10)
}
}
}
}
}
.padding()
.onAppear {
viewModel.fetchCategories()
}
}
VStack {
Spacer()
if isEditing {
NavigationLink(destination: AddCategoryView()) {
Text("新增分類")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.padding(.horizontal)
}
.padding(.bottom, 20)
}
}
}
.navigationTitle("分類列表")
.toolbar {
Button(action: {
isEditing.toggle()
}) {
Image(systemName: isEditing ? "checkmark" : "square.and.pencil")
.font(.title2)
.foregroundColor(.blue)
}
}
.navigationBarTitleDisplayMode(.inline)
.toast(isPresenting: $viewModel.failHandle.isFail, alert: {
AlertToast(type: .error(Color.red), title: viewModel.failHandle.title)
})
}
}
#Preview {
CategoryListView()
}
這樣就完成啦!
今天我們成功完成分類列表頁面的實作,讓使用者可以瀏覽、刪除分類,並進入編輯模式管理分類。這樣的設計不僅提升了使用者的互動性,還讓分類管理變得更加直覺。在下一篇文章中,我們將繼續實作新增分類的功能,敬請期待!